import React, { useState, useRef, useEffect, useMemo } from 'react';
import ReactDOM from 'react-dom';
import faker from 'faker';
import './virtualList.css';

const listData = [];
for (let i = 0; i < 1000; i++) {
  listData.push({
    key: i.toString(),
    val: faker.lorem.sentences(), // 长文本
    _index: `_${i}`,
  });
}

const itemSize = 50; // 列表每项高度
// const listHeight = listData.length * itemHeight; // 列表总高度
const estimatedItemSize = 100; // 预估高度
const bufferScale = 1; // 缓冲区比例

const VirtualList = () => {
  const ref = useRef();
  const itemsWrapperRef = useRef();
  const [info, setInfo] = useState({
    visibleData: [], // 可视区域数据
    transform: '',
    positions: [
      // {
      //   top:0,
      //   bottom:100,
      //   height:100
      // }
    ], // 用于列表项渲染后存储每一项的高度以及位置信息
  });

  const listHeight = useMemo(() => {
    const { positions = [] } = info;
    return positions[positions.length - 1]?.bottom;
  }, [info]); // 列表总高度

  // 二分查找
  const binarySearch = (list, value) => {
    let start = 0;
    let end = list.length - 1;
    let tempIndex = null;
    while (start <= end) {
      let middIndex = Math.floor((start + end) / 2);
      let middValue = list[middIndex].bottom;
      if (middValue === value) {
        return middIndex + 1;
      } else if (middValue < value) {
        start = middIndex + 1;
      } else if (middValue > value) {
        if (tempIndex === null || tempIndex > midIndex) {
          tempIndex = midIndex;
        }
        end = end - 1;
      }
    }
    return tempIndex;
  };

  // 获取列表起始索引
  const getStartIndex = (scrollTop = 0) => {
    let item = info.positions.find((el) => el && el.bottom > scrollTop);
    return item.index;
    // 由于我们的缓存数据，本身就是有顺序的，所以获取开始索引的方法可以考虑通过二分查找的方式来降低检索次数：
    // return binarySearch(info.positions, scrollTop);
  };

  const updateViewContent = (scrollTop = 0) => {
    // 计算可视区域里能放几个元素
    const visibleCount = Math.ceil(ref.current.clientHeight / itemSize);
    // 计算可视区域开始的索引
    // const startIndex = Math.floor(scrollTop / itemSize);
    const startIndex = getStartIndex(scrollTop);
    // 获取偏移量
    // const transform = startIndex * itemSize
    const transform =
      startIndex >= 1 ? info.positions[startIndex - 1].bottom : 0;
    // 计算可视区域结束的索引
    const endIndex = startIndex + visibleCount;
    // 可视区上方渲染条数
    const aboveCount = Math.min(startIndex, bufferScale * visibleCount);
    // 可视区下方渲染条数
    const belowCount = Math.min(
      listData.length - endIndex,
      bufferScale * visibleCount
    );
    // 获取可视区域数据
    // const visibleData = listData.slice(
    //   startIndex - aboveCount,
    //   endIndex + belowCount
    // );
    const visibleData = listData.slice(startIndex, endIndex);

    setInfo({
      ...info,
      visibleData,
      transform: `translate3d(0, ${transform}px, 0)`,
    });
  };

  const initPositions = () => {
    info.positions = listData.map((item, index) => {
      return {
        index,
        height: estimatedItemSize,
        top: index * estimatedItemSize,
        bottom: (index + 1) * estimatedItemSize,
      };
    });
    setInfo({
      ...info,
    });
  };

  // 获取列表项的当前尺寸
  const updatePositions = () => {
    const nodes = itemsWrapperRef.current.childNodes;
    // console.log('nodes: ', nodes);
    if (!nodes.length) return;
    // 获取真实元素大小，修改对应的尺寸缓存
    nodes.forEach((node) => {
      let rect = node.getBoundingClientRect();
      let height = rect.height;
      let index = +node.id.slice(1);
      let oldHeight = info.positions[index].height;
      let dValue = oldHeight - height;
      // 存在差值
      if (dValue) {
        let _positions = info.positions;
        _positions[index].bottom = _positions[index].bottom - dValue;
        _positions[index].height = height;
        for (let i = index + 1; i < _positions.length; i++) {
          _positions[i].top = _positions[i - 1].bottom;
          _positions[i].bottom = _positions[i].bottom - dValue;
        }
        setInfo({
          ...info,
          positions: _positions,
        });
      }
    });
  };

  const handleScroll = () => {
    /*
     * 获取scrollTop
     * 此属性可以获取或者设置对象的最顶部到对象在当前窗口显示的范围内的顶边的距离
     * 也就是元素滚动条被向下拉动的距离
     * */
    updateViewContent(ref.current.scrollTop);
  };

  useEffect(() => {
    initPositions();
    updateViewContent();
  }, []);

  useEffect(() => {
    updatePositions();
  });

  return (
    <>
      <div
        className="infinite-list-container"
        ref={ref}
        onScroll={handleScroll}
      >
        <div
          className="infinite-list-phantom"
          style={{ height: listHeight + 'px' }}
        ></div>
        <div
          ref={itemsWrapperRef}
          className="infinite-list"
          style={{
            transform: info.transform,
          }}
        >
          {info.visibleData.map((item) => (
            <div
              className="infinite-list-item"
              id={item._index}
              key={item.key}
              // style={{
              //   height: itemSize + 'px',
              //   lineHeight: itemSize + 'px',
              // }}
            >
              <span style={{ color: 'red' }}>{item.key}</span>
              {item.val}
            </div>
          ))}
        </div>
      </div>
    </>
  );
};

ReactDOM.render(<VirtualList />, document.getElementById('root'));
